package com.example.jpa.crud.search.expression;

import com.example.jpa.crud.exception.ExpressionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;

public class SortExpressionResolver {

    private static final Logger log = LoggerFactory.getLogger(SortExpressionResolver.class);

    public static List<String[]> resolve(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        final List<Token> tokens = getTokens(text);
        final TokenReader reader = new TokenReader(tokens);
        List<String[]> list = null;
        try {
            list = getValueDescriptionList(reader);
        } catch (ExpressionException e) {
            log.error("'{}' cannot be parsed correctly, {}", text, e.getMessage());
        }
        return list;
    }

    private enum TokenType {
        IDENTIFIER,
        DESCRIPTION,
        COMMA
    }

    private static class Token {
        TokenType type;
        String value;

        Token(TokenType type, String value) {
            this.type = type;
            this.value = value;
        }

        static Token COMMA = new Token(TokenType.COMMA, ",");
        static Token DESC = new Token(TokenType.DESCRIPTION, "desc");
        static Token ASC = new Token(TokenType.DESCRIPTION, "asc");
    }

    private static Token buildToken(int[] codePoints, int startIndex, int endIndex) {
        final String identifier = new String(codePoints, startIndex, endIndex - startIndex);
        final String identifierUpperCase = identifier.toUpperCase();
        if ("DESC".equals(identifierUpperCase)) {
            return Token.DESC;
        } else if ("ASC".equals(identifierUpperCase)) {
            return Token.ASC;
        } else {
            return new Token(TokenType.IDENTIFIER, identifier);
        }
    }

    private static List<Token> getTokens(String text) {
        final int textLength = text.length();
        final int[] codePoints = new int[textLength];
        int length = 0;
        int i = 0;
        int codePoint;
        while (i < textLength) {
            codePoint = text.codePointAt(i);
            codePoints[length++] = codePoint;
            i += Character.charCount(codePoint);
        }
        final List<Token> tokens = new ArrayList<>();
        Token token;
        int startIndex = -1;
        i = 0;
        while (i < length) {
            if (Character.isWhitespace(codePoints[i])) {
                if (startIndex > -1) {
                    token = buildToken(codePoints, startIndex, i);
                    if (token != null) {
                        tokens.add(token);
                    }
                    startIndex = -1;
                }
            } else {
                if (codePoints[i] == ',') {
                    if (startIndex > -1) {
                        token = buildToken(codePoints, startIndex, i);
                        if (token != null) {
                            tokens.add(token);
                        }
                        startIndex = -1;
                    }
                    tokens.add(Token.COMMA);
                } else {
                    if (startIndex == -1) {
                        startIndex = i;
                    }
                }
            }
            i++;
        }
        if (startIndex > -1) {
            token = buildToken(codePoints, startIndex, length);
            if (token != null) {
                tokens.add(token);
            }
        }
        return tokens;
    }


    private static class TokenReader {
        private final List<Token> tokens;
        private int i;

        TokenReader(List<Token> tokens) {
            this.tokens = tokens;
            this.i = 0;
        }

        Token peek() {
            if (this.i >= 0 && this.i < tokens.size()) {
                return tokens.get(this.i);
            } else {
                return null;
            }
        }

        void forward() {
            if (this.i >= 0 && this.i < tokens.size()) {
                this.i++;
            }
        }

        void backward() {
            if (this.i > 0) {
                this.i--;
            }
        }
    }

    private static List<String[]> getValueDescriptionList(TokenReader reader) throws ExpressionException {
        final List<String[]> list = new ArrayList<>();
        Token token = reader.peek();
        while (token != null) {
            if (token.type == TokenType.IDENTIFIER) {
                final String identifier = token.value;
                reader.forward();
                token = reader.peek();
                if (token == null || token.type != TokenType.DESCRIPTION) {
                    throw new ExpressionException("expect 'ASC' (or 'DESC')");
                }
                list.add(new String[]{identifier, token.value});
            }
            reader.forward();
            token = reader.peek();
        }
        return list;
    }

}
